﻿using Devil;
using GameToolkit.UI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;

namespace GameToolkit
{
    using ExpendAxis = AsyncDataListView.ExpendAxis;

    public class FixedDataListView : LayoutGroup, IDataList
    {
        [SerializeField, Tooltip("自上而下，然后自左而右排列")]
        bool m_VerticalThenHorizontal;

        [SerializeField]
        ExpendAxis m_ExpendChildren;

        [SerializeField, Tooltip("限定每行最大数量")]
        int m_MaxRowViews;

        [SerializeField]
        Vector2 m_ItemSpace;

        bool mInitialized;
        RandomList<Data> mBuffer = new RandomList<Data>();

        List<Data> mDatas = new List<Data>();

        public int DataCount => mDatas.Count;

        void Initialize()
        {
            if (mInitialized)
                return;
            mInitialized = true;
            for (int i = transform.childCount - 1; i >= 0; i--)
            {
                var trans = transform.GetChild(i) as RectTransform;
                trans.anchorMin = new Vector2(0, 0);
                trans.anchorMax = new Vector2(0, 0);
                trans.pivot = new Vector2(0, 0);
                var dt = new Data(trans.gameObject);
                mBuffer.Add(dt);
#if UNITY_EDITOR
                if (Application.isPlaying)
#endif
                    dt.gameObject.SetActive(false);
            }
            mDatas.Clear();
        }

        public void AddData(object data)
        {
            Initialize();
            Data obj = null;
            if (mBuffer.Count > 0)
            {
                obj = mBuffer.RemoveAt(0);
            }
            else if (mDatas.Count > 0)
            {
                var go = Instantiate(mDatas[0].gameObject, transform);
                obj = new Data(go);
            }
            if (obj == null)
                return;
            obj.data = data;
            if (obj.dataSource != null)
                obj.dataSource.SetData(data);
            obj.gameObject.SetActive(true);
            mDatas.Add(obj);
            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
        }

        public void BindData(IEnumerable iter)
        {
            ClearData();
            Initialize();
            foreach (var dt in iter)
            {
                Data obj = null;
                if (mBuffer.Count > 0)
                {
                    obj = mBuffer.RemoveAt(0);
                }
                else if (mDatas.Count > 0)
                {
                    var go = Instantiate(mDatas[0].gameObject, transform);
                    obj = new Data(go);
                }
                if (obj == null)
                    break;
                obj.data = dt;
                if (obj.dataSource != null)
                    obj.dataSource.SetData(dt);
                obj.gameObject.SetActive(true);
                mDatas.Add(obj);
            }
            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
        }

        public void ClearData()
        {
            for (int i = 0; i < mDatas.Count; i++)
            {
                if (mDatas[i].gameObject != null)
                {
                    mBuffer.Add(mDatas[i]);
                    mDatas[i].gameObject.SetActive(false);
                }
            }
            mDatas.Clear();
        }

        public object GetData(int index)
        {
            return mDatas[index].data;
        }

        public T GetDataComponent<T>(int index) where T : class
        {
            T t;
            if (mDatas[index].gameObject.TryGetComponent(out t))
                return t;
            else
                return null;
        }

        public GameObject GetDataInstance(int index)
        {
            return mDatas[index].gameObject;
        }

        public IEnumerator GetEnumerator()
        {
            return new WrapEnumerator<Data, object>(mDatas.GetEnumerator(), (dt) => dt.data);
        }

        public void RemoveAt(int index)
        {
            var dt = mDatas[index];
            mDatas.RemoveAt(index);
            if (dt.gameObject != null)
            {
                mBuffer.Add(dt);
                dt.gameObject.SetActive(false);
            }
            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
        }

        public void RemoveData(object data)
        {
            for (int i = 0; i < mDatas.Count; i++)
            {
                if (mDatas[i].data == data)
                {
                    RemoveAt(i);
                    return;
                }
            }
        }

        public void UpdateData(object data)
        {
            for (int i = 0; i < mDatas.Count; i++)
            {
                if (mDatas[i].data == data)
                {
                    mDatas[i].dataSource?.ApplyUpdate();
                    return;
                }
            }
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            mInitialized = false;
        }

        public override void CalculateLayoutInputVertical()
        {
        }

        public override void SetLayoutHorizontal()
        {
        }

        public override void SetLayoutVertical()
        {
#if UNITY_EDITOR
            m_Tracker.Clear();
            DrivenTransformProperties drive = DrivenTransformProperties.AnchoredPosition | DrivenTransformProperties.AnchorMin | DrivenTransformProperties.AnchorMax | DrivenTransformProperties.Pivot;
            for (int i = 0; i < transform.childCount; i++)
            {
                var trans = transform.GetChild(i) as RectTransform;
                m_Tracker.Add(this, trans, drive);
                trans.anchorMin = new Vector2(0, 0);
                trans.anchorMax = new Vector2(0, 0);
                trans.pivot = new Vector2(0, 0);
            }
            bool reset = !mInitialized || !Application.isPlaying;
#else
            bool reset = !mInitialized;
#endif
            if (reset)
            {
                mDatas.Clear();
                for (int i = 0; i < transform.childCount; i++)
                {
                    var go = transform.GetChild(i).gameObject;
                    if (go.activeSelf)
                        mDatas.Add(new Data(go));
                }
            }
            var rect = rectTransform.rect;
            rect = new Rect(rect.xMin + padding.left, rect.yMin + padding.bottom, rect.width - padding.right - padding.left, rect.height - padding.top - padding.bottom);
            if (m_VerticalThenHorizontal)
            {
                DoVerticalRowLayout(rect);
            }
            else
            {
                DoHorizontalRowLayout(rect);
            }
        }

        Vector2 GetAlignPivot(TextAnchor anchor)
        {
            switch(anchor)
            {
                case TextAnchor.LowerCenter:
                    return new Vector2(0.5f, 0f);
                case TextAnchor.LowerLeft:
                    return new Vector2(0, 0);
                case TextAnchor.LowerRight:
                    return new Vector2(1f, 0);
                case TextAnchor.MiddleCenter:
                    return new Vector2(0.5f, 0.5f);
                case TextAnchor.MiddleLeft:
                    return new Vector2(0f, 0.5f);
                case TextAnchor.MiddleRight:
                    return new Vector2(1f, 0.5f);
                case TextAnchor.UpperCenter:
                    return new Vector2(0.5f, 1f);
                case TextAnchor.UpperLeft:
                    return new Vector2(0f, 1f);
                case TextAnchor.UpperRight:
                    return new Vector2(1f, 1f);
                default:
                    return new Vector2(0, 0);
            }
        }

        void FinishHorizontalRow(Rect area, List<Rect> rects, Vector2 align, ref Rect contentRect, ref Rect rowRect, int rowStart, int rowCount)
        {
            contentRect.xMax = Mathf.Max(contentRect.xMax, rowRect.xMax);
            contentRect.yMin = Mathf.Min(contentRect.yMin, rowRect.yMin);
            float expend, offsetX;
            if ((m_ExpendChildren == ExpendAxis.Both || m_ExpendChildren == ExpendAxis.Horizontal) && rowCount > 1 && area.width > rowRect.width)
            {
                expend = (area.width - rowRect.width) / (rowCount - 1);
                offsetX = 0;
            }
            else
            {
                offsetX = (area.width - rowRect.width) * align.x;
                expend = 0;
            }
            for (int k = 0; k < rowCount; k++)
            {
                var newrect = rects[k + rowStart];
                newrect.x += offsetX + expend * k;
                newrect.y += (newrect.height - rowRect.height) * (1 - align.y);
                rects[k + rowStart] = newrect;
            }
            rowRect = new Rect(area.xMin, rowRect.yMin - m_ItemSpace.y, 0, 0);
        }

        void DoHorizontalRowLayout(Rect area)
        {
            Rect rowRect = new Rect(area.xMin, area.yMax, 0, 0);
            Rect contentRect = rowRect;
            var lst = CachedList<Rect>.GetList();
            var rows = CachedList<int>.GetList();
            int rowStart = 0;
            var end = mDatas.Count - 1;
            var align = GetAlignPivot(m_ChildAlignment);
            for (int i = 0; i <= end; i++)
            {
                var trans = mDatas[i].gameObject.transform as RectTransform;
                var size = trans.sizeDelta;
                var rect = new Rect(rowRect.xMax + (i == 0 ? 0 : m_ItemSpace.x), rowRect.yMax - size.y, size.x, size.y);
                // next line
                if (i > rowStart && rect.xMax > area.xMax || m_MaxRowViews > 0 && i - rowStart >= m_MaxRowViews)
                {
                    rect.x = area.xMin;
                    rect.y = rowRect.yMin - m_ItemSpace.y - size.y;
                    FinishHorizontalRow(area, lst, align, ref contentRect, ref rowRect, rowStart, i - rowStart);
                    rowStart = i;
                    rows.Add(i);
                }
                rowRect.xMax = Mathf.Max(rowRect.xMax, rect.xMax);
                rowRect.yMin = Mathf.Min(rowRect.yMin, rect.yMin);
                lst.Add(rect);
            }
            FinishHorizontalRow(area, lst, align, ref contentRect, ref rowRect, rowStart, mDatas.Count - rowStart);
            rows.Add(mDatas.Count);
            float expend, offsetY;
            if ((m_ExpendChildren == ExpendAxis.Both || m_ExpendChildren == ExpendAxis.Vertical) && rows.Count > 1 && area.height > contentRect.height)
            {
                expend = -(area.height - contentRect.height) / (rows.Count - 1);
                offsetY = 0;
            }
            else
            {
                expend = 0;
                offsetY = (contentRect.height - area.height) * (1 - align.y);
            }
            if (!Mathf.Approximately(offsetY, 0) || !Mathf.Approximately(expend, 0))
            {
                int rowIndex = 0;
                for (int i = 0; i < lst.Count; i++)
                {
                    if (i >= rows[rowIndex])
                        rowIndex++;
                    var newrect = lst[i];
                    newrect.y += offsetY + expend * rowIndex;
                    lst[i] = newrect;
                }
            }

            for (int i = 0; i < mDatas.Count; i++)
            {
                var trans = (RectTransform)mDatas[i].gameObject.transform;
                trans.localPosition = lst[i].position;
            }
            CachedList<Rect>.Release(lst);
        }

        void FinishVerticalRow(Rect area, List<Rect> rects, Vector2 align, ref Rect contentRect, ref Rect rowRect, int rowStart, int rowCount)
        {
            contentRect.xMax = Mathf.Max(contentRect.xMax, rowRect.xMax);
            contentRect.yMin = Mathf.Min(contentRect.yMin, rowRect.yMin);
            float expend, offsetY;
            if ((m_ExpendChildren == ExpendAxis.Both || m_ExpendChildren == ExpendAxis.Vertical) && rowCount > 1 && area.height > rowRect.height)
            {
                expend = -(area.height - rowRect.height) / (rowCount - 1);
                offsetY = 0;
            }
            else
            {
                offsetY = -(area.height - rowRect.height) * (1 - align.y);
                expend = 0;
            }
            for (int k = 0; k < rowCount; k++)
            {
                var newrect = rects[k + rowStart];
                newrect.x += (rowRect.width - newrect.width) * align.x;
                newrect.y += offsetY + expend * k;
                rects[k + rowStart] = newrect;
            }
            rowRect = new Rect(rowRect.xMax + m_ItemSpace.x, area.yMax, 0, 0);
        }

        void DoVerticalRowLayout(Rect area)
        {
            Rect rowRect = new Rect(area.xMin, area.yMax, 0, 0);
            Rect contentRect = rowRect;
            var lst = CachedList<Rect>.GetList();
            var rows = CachedList<int>.GetList();
            int rowStart = 0;
            var end = mDatas.Count - 1;
            var align = GetAlignPivot(m_ChildAlignment);
            for (int i = 0; i <= end; i++)
            {
                var trans = mDatas[i].gameObject.transform as RectTransform;
                var size = trans.sizeDelta;
                var rect = new Rect(rowRect.xMin, rowRect.yMin - size.y - (i == 0 ? 0 : m_ItemSpace.y), size.x, size.y);
                // next line
                if (i > rowStart && rect.yMin < area.yMin || m_MaxRowViews > 0 && i - rowStart >= m_MaxRowViews)
                {
                    rect.x = rowRect.xMax + m_ItemSpace.x;
                    rect.y = area.yMax - size.y;
                    FinishVerticalRow(area, lst, align, ref contentRect, ref rowRect, rowStart, i - rowStart);
                    rowStart = i;
                    rows.Add(i);
                }
                rowRect.xMax = Mathf.Max(rowRect.xMax, rect.xMax);
                rowRect.yMin = Mathf.Min(rowRect.yMin, rect.yMin);
                lst.Add(rect);
            }
            FinishVerticalRow(area, lst, align, ref contentRect, ref rowRect, rowStart, mDatas.Count - rowStart);
            rows.Add(mDatas.Count);
            float expend, offsetX;
            if ((m_ExpendChildren == ExpendAxis.Both || m_ExpendChildren == ExpendAxis.Horizontal) && rows.Count > 1 && area.width > contentRect.width)
            {
                expend = (area.width - contentRect.width) / (rows.Count - 1);
                offsetX = 0;
            }
            else
            {
                expend = 0;
                offsetX = (area.width - contentRect.width) * align.x;
            }
            if (!Mathf.Approximately(offsetX, 0) || !Mathf.Approximately(expend, 0))
            {
                int rowIndex = 0;
                for (int i = 0; i < lst.Count; i++)
                {
                    if (i >= rows[rowIndex])
                        rowIndex++;
                    var newrect = lst[i];
                    newrect.x += offsetX + expend * rowIndex;
                    lst[i] = newrect;
                }
            }

            for (int i = 0; i < mDatas.Count; i++)
            {
                var trans = (RectTransform)mDatas[i].gameObject.transform;
                trans.localPosition = lst[i].position;
            }
            CachedList<Rect>.Release(lst);
        }

        class Data
        {
            internal readonly GameObject gameObject;
            internal readonly IDataSource dataSource;
            internal object data;
            internal Data(GameObject obj)
            {
                gameObject = obj;
                obj.TryGetComponent(out dataSource);
            }
        }
    }
}
